# frozen_string_literal: true

class KlotskiGui < FXMainWindow
  class GuiBoard < ShapeCanvas
    def self.nu_for_board(parent, size_factor, board = nil)
      return nu_for_board(parent, size_factor, Game.instance.initial_board) unless board

      board.geometry.values_at(:rows, :columns).tap do |rows, columns|
        return new(parent, rows, columns, size_factor).reset(board)
      end
    end

    def initialize(parent, rows, columns, size_factor)
      @rows      = rows
      @columns   = columns
      @unit_size = 2**size_factor
      @pieces    = {}
      @listeners = []

      super(
        parent,
        nil,
        0,
        LAYOUT_FIX_WIDTH | LAYOUT_FIX_HEIGHT
      )
      self.width     = @columns * @unit_size + 1
      self.height    = @rows    * @unit_size + 1
      self.backColor = 'grey'
      self.scene = ShapeGroup.new

      connect(SEL_LEFTBUTTONRELEASE) do |_, _, event|
        on_clicked(event)
      end
    end

    def reset(board)
      @rows,
      @columns = board.geometry.values_at(:rows, :columns)

      self.width     = @columns * @unit_size + 1
      self.height    = @rows    * @unit_size + 1

      self.scene = ShapeGroup.new
      draw_board(board)
      update
      return self
    end

    def on_clicked(event)
      return if findShape(event.click_x, event.click_y)

      scene.find(&:selected?).tap do |selected|
        dispatch(
          :on_move,
          @pieces.key(selected),
          selected.click_direction(event.click_x, event.click_y)
        )
      end
    end

    def move_piece(id, dir)
      return move_shape(@pieces[id], dir)
    end

    def move_shape(shape, dir)
      return nil unless dir && shape

      case dir
      when :up    then [0, -1]
      when :down  then [0, 1]
      when :left  then [-1, 0]
      when :right then [1, 0]
      end.tap do |dx, dy|
        shape.move(
          shape.x + (dx * @unit_size),
          shape.y + (dy * @unit_size)
        )
        update
        return dir
      end
    end

    def draw_board(board)
      Game.instance.piece_chars.collect do |c|
        [
          c,
          PieceMask.nu(board, c).piece_geometry(true)
        ]
      end.each do |c, geometry|
        draw_piece(
          c,
          geometry,
          geometry.values_at(:rows, :columns) == [2, 2] ? 'red' : 'white'
        )
      end
      return self
    end

    def draw_piece(id, geometry, color)
      scene.addShape(
        @pieces[id] = GuiPiece.new(geometry, color, @unit_size)
      )
      return self
    end

    def add_listener(listener)
      @listeners << listener
      return self
    end

    def dispatch(method, *args)
      @listeners.select {|l| l.respond_to?(method)}.each {|l| l.send(method, *args)}
    end
  end

  class GuiPiece < RectangleShape
    INTERNAL_BORDER = 5

    def initialize(geometry, color, unit_size)
      super(
        geometry[:x] * unit_size,
        geometry[:y] * unit_size,
        (geometry[:columns] * unit_size) - 1,
        (geometry[:rows]    * unit_size) - 1
      )
      @color = color
      self.foreground = @color
    end

    def click_direction(click_x, click_y)
      {
        :x => (((click_x <=> x) + (click_x <=> x + width)) / 2.0).truncate,
        :y => (((click_y <=> y) + (click_y <=> y + height)) / 2.0).truncate
      }.tap do |cmp|
        return false unless cmp[:x].zero? ^ cmp[:y].zero?

        return (
          if cmp[:x].zero?
            cmp[:y].negative? ? :up : :down
          else
            cmp[:x].negative? ? :left : :right
          end
        )
      end
    end

    def draw(dc)
      dc.foreground = foreground
      dc.fillRectangle(x, y, bounds.w, bounds.h)

      dc.foreground = 'black'
      dc.drawRectangle(x, y, bounds.w, bounds.h)

      return unless selected?

      dc.foreground = 'grey'
      dc.drawRectangle(
        x + INTERNAL_BORDER,
        y + INTERNAL_BORDER,
        bounds.w - 2 * INTERNAL_BORDER,
        bounds.h - 2 * INTERNAL_BORDER
      )
    end
  end

  class BoardObservable < Observable
    def initialize(gui_board)
      super()
      @gui_board = gui_board.add_listener(self)
    end

    def []=(name, value)
      case name
      when :board then @gui_board.reset(value)
      when :next_move
        puts(value.to_s)
        @gui_board.move_piece(value.c, value.direction)
      else
        super
      end
    end

    def on_move(c, direction)
      dispatch_event(nil, c, direction)
    end
  end
end
